import time
import hashlib
from tornado import web, ioloop, gen
from datetime import datetime
import numpy as np
import traceback
import json
import logging


class Session:
    def __init__(self, expire_seconds: float):
        self._created = time.time()
        self._expire_seconds = expire_seconds
        self._id = hashlib.md5(str(self._created).encode()).hexdigest()
        self._next = None
        self._prev = None
        self._attr = {}

    def set_attr(self, name, value):
        self._attr[name] = value

    def get_attr(self, name):
        return self._attr[name]

    def has_attr(self, name):
        return name in self._attr

    def __contains__(self, name):
        return name in self._attr

    def __getitem__(self, name):
        return self._attr[name]

    def __setitem__(self, name, value):
        self._attr[name] = value

    @property
    def created(self):
        """
        Return the time that this Session is created, which is a float in seconds
        :return: the time that this Session is created
        """
        return self._created

    @property
    def id(self):
        return self._id

    @property
    def expire_seconds(self):
        """
        The time in seconds that this Session keeps alive after the last request from client side.
        :return: the time in seconds that this Session keeps alive after the last request from client side.
        """
        return self._expire_seconds

    def set_expire_seconds(self, expire_seconds: float):
        self._expire_seconds = expire_seconds


PSESSION_ID = 'PSESSIONID'
_session_expire_seconds = 20 * 60
_session_map = {}
_session_head = Session(0)
_session_head._next = _session_head
_session_head._prev = _session_head


def get_session_expire_seconds():
    global _session_expire_seconds
    return _session_expire_seconds


def set_session_expire_seconds(seconds: float):
    global _session_expire_seconds
    _session_expire_seconds = seconds


def _get_session(sid):
    now = time.time()
    p = _session_head._prev
    while p is not _session_head and p._created + p._expire_seconds <= now:
        if p._id in _session_map:
            del _session_map[p._id]
        p = p._prev
    p._next = _session_head
    _session_head._prev = p

    if sid not in _session_map:
        return None
    session = _session_map[sid]
    session._prev._next = session._next
    session._next._prev = session._prev

    session._created = now
    session._next = _session_head._next
    session._prev = _session_head
    _session_head._next = session
    session._next._prev = session
    return session


def _save_session(session):
    _session_map[session._id] = session
    session._next = _session_head._next
    session._prev = _session_head
    _session_head._next = session
    session._next._prev = session


class ErrException(Exception):
    def __init__(self, err_code, err_msg):
        super(ErrException, self).__init__(err_msg)
        self.err_code = err_code
        self.err_msg = err_msg


class SessionHandler(web.RequestHandler):
    """
    This handler is the parent class of all the handlers that needs the web of session.
    A handler programmer can use the Session object to store any data that is active all the life time
    of the session, such as the user login name or the account.
    The session is unique to different client side and is
    active only if the corresponding client side sends at least one request to a SessionHandler
    in 1200 seconds. Otherwise the session object is expired and therefore removed.
    The default expire time (1200 seconds) can be updated globally by method
    set_session_expire_seconds() which will effect only future session objects, and does nothing to
    all the current sessions.
    """

    def __init__(self, application, request, **kwargs):
        super(SessionHandler, self).__init__(application, request, **kwargs)
        self._session = None
        self.json_response = True

    def get_session(self, session_class=Session, expire_seconds=None, *args, **kwargs):
        """
        Get the Session object of the HTTP request sent to this handler. The requests
        from the same client side share a same Session object. Please call this function and then
        set the session attributes such as the user login name into the Session object.
        :param session_class: if the Session object is not found, this class is used to construct the Session object.
        :param args: the args past to the Session constructor if the Session object is not found.
        :param kwargs: the kwargs past to the Session constructor if the Session object is not found.
        :return: the Session object of the HTTP request sent to this handler.
        """
        if expire_seconds is None:
            expire_seconds = get_session_expire_seconds()
        if self._session is None:
            sid = self.get_argument(PSESSION_ID, None)
            if sid is None:
                sid = self.get_cookie(PSESSION_ID)
            session = None if sid is None else _get_session(sid)
            if session is None:
                session = session_class(expire_seconds, *args, **kwargs)
                _save_session(session)
                self.set_cookie(PSESSION_ID, session.id)
            self._session = session
        return self._session

    def initialize(self, json_response: bool = True) -> None:
        self.json_response = json_response

    def async_process(self, *args):
        return None

    def _process(self, *args):
        res = {}
        try:
            result = self.async_process(*args)
            if not self.json_response:
                return result

            res['result'] = result
            res['err_code'] = 0
            res['err_msg'] = None
        except ErrException as e:
            res['err_msg'] = e.err_msg
            res['err_code'] = e.err_code
        except Exception as e:
            s = str(traceback.format_exc())
            logging.getLogger().error(s)
            res['err_msg'] = e.err_msg if hasattr(e, 'err_msg') else (str(e) + ':\n' + s)
            res['err_code'] = e.err_code if hasattr(e, 'err_code') else 9999

        # if 'Origin' in self.request.headers:
            # origin = self.request.headers['Origin']
            # self.set_header("Access-Control-Allow-Origin", origin)
        self.set_header("Access-Control-Allow-Credentials", "true")
        self.set_header("Access-Control-Allow-Origin", '*')
        self.set_header("Access-Control-Allow-Headers", 'x-requested-with, content-type, authorization')
        self.set_header("Access-Control-Allow-Methods", 'GET,POST,OPTIONS')

        self.set_header("Content-type", "application/json")
        self.write(json.dumps(res, cls=DatetimeEncoder))

    def get(self, *args):
        return ioloop.IOLoop.current().run_in_executor(None, self._process, args)

    def post(self, *args):
        return ioloop.IOLoop.current().run_in_executor(None, self._process, args)


class DatetimeEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.strftime('%Y-%m-%d %H:%M:%S')
        type1 = type(obj)
        if type1 == np.ndarray:
            return json.dumps(list(obj))
        if type1 in (np.bool, np.bool_):
            return str(bool(obj)).lower()
        if type1 in (np.int, np.int_, np.intc):
            return str(int(obj))
        if type1 in (np.float, np.float_, np.float32):
            return str(float(obj))
        if type1 == np.str:
            return json.dumps(str(obj))
        return json.JSONEncoder.default(self, obj)
